﻿using System;
using System.IO;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;



namespace AIthin.Neuro
{

    [Serializable]
    public class Network : INetwork
    {
        unsafe struct cuPointers
        {
            public int* n;
            public cuEmbryo* e;
        }

        unsafe struct cuEmbryo
        {
            public int was_activ;
            public int inputs_count;
            /// <summary>
            /// max 16 conections
            /// </summary>
            public fixed int connections[16];
        }

        [DllImport("AIthin.CUDA.dll",
        ExactSpelling = false,
        CallingConvention = CallingConvention.Cdecl)]

        unsafe static extern cuPointers check_embryos(
            int* neurons,
            int neurons_count,
            cuEmbryo* embryos,
            int embryos_count);

        [DllImport("AIthin.CUDA.dll",
        ExactSpelling = false,
        CallingConvention = CallingConvention.Cdecl)]

        unsafe static extern int end_check_embryos(
            int* dev_neurons,
            cuEmbryo* embryos,
            cuEmbryo* dev_embryos,
            int embryos_count);


        /// <summary>
        /// size of input square f.ex. 32x32 inputSize is 32
        /// </summary>
        int inputSize;
        /// <summary>
        /// Lifetime this Network in Cycles or Waves was computetad
        /// </summary>
        public int LifeTime { get; protected set; }
        protected List<int> ComputatedNeurons;
        protected List<int> ActivatedNeurons;
        protected List<EmbryoNeuron> EmbryoNeurons;
        public int[][] ActivityQueue;
        public int QueueCount = 15;
        //        float LearnWeight = 0.2f;
        int maxEmbryoLifeTime = 1000;
        public int inputsCount { get; protected set; }
        public int outputsCount { get; protected set; }
        protected int basicNeuronsCount { get; set; }
        public int NeuronsCount { get; protected set; }
        protected static Random random = new Random();
        internal Neuron[] neurons;
        public INeuron this[int index]
        {
            get { return (INeuron)neurons[index]; }
        }

       
        public delegate void WaveComputed(object sender, string e);
        public event WaveComputed NeuroWaveComputed;

        public List<Layer> layers { get; protected set; }

        /// <summary>
        /// dummy Network (must be filled)
        /// </summary>
        public Network()
        {
            LifeTime = 0;
            ComputatedNeurons = new List<int>();
            ActivatedNeurons = new List<int>();
            ActivityQueue = new int[QueueCount][];
            EmbryoNeurons = new List<EmbryoNeuron>();
            layers = new List<Layer>();
        }
        /// <summary>
        /// create network
        /// </summary>
        /// <param name="nc">base neurons count </param>
        /// <param name="inputs">input neurons count</param>
        /// <param name="outputs">outputs neuron count</param>
        public Network(int nc, int inputs, int outputs) : this()
        {
            this.inputsCount = Math.Max(1, inputs);
            this.outputsCount = Math.Max(1, outputs);
            this.basicNeuronsCount = Math.Max(1, nc);
            NeuronsCount = inputsCount; 
            // create collection of neurons
            neurons = new Neuron[NeuronsCount];

            double Size = Math.Sqrt((double)inputsCount);

            //TODO andere input layer vorgehensweise realisieren
            if (inputsCount != Math.Pow(Size, 2d))
                initInputNeurons();
            else
            {
                inputSize = (int)Size;
                initInputNeurons(inputSize);
            }

            //init second layer
            Layer middle = new Layer (this, 15f, 15f, 15f, 30f, 30f, nc, 15.5f, 8);
            //init output layer
            new Layer (this, middle, 15f, 15f, 30f, 30f, 30f, outputs, 8);

       
 
        }
        private void initInputNeurons()
        {
            // The first neurons at Input, amount -> InputsCount
            for (int i = 0; i < inputsCount; i++)
            {
                this.neurons[i] = new Neuron(i, 1, 8);
                // set positions
                double Size = Math.Sqrt((double)inputsCount);

                if (Size != Math.Pow((double)inputsCount, 2d))
                {
                    neurons[i].X = (float)(random.NextDouble() * 2d);
                    neurons[i].Y = (float)(random.NextDouble() * 2d);
                }
                else
                {
                    neurons[i].X = (float)(random.NextDouble() * 2d);
                    neurons[i].Y = (float)(random.NextDouble() * 2d);
                }

                neurons[i].Z = 0f;
                Debug.WriteLine(neurons[i].ID, "created input neuron random");
            }

        }
        /// <summary>
        /// create Square input field
        /// </summary>
        /// <param name="Size">nuumber neurons at side</param>
        private void initInputNeurons(int Size)
        {
            // raster
            float Step = 1.0f;
            // The first neurons at Input, amount -> InputsCount
            int k = 0;
            for (int j = 0; j < Size; j++)
            {
                for (int i = 0; i < Size; i++)
                {
                    this.neurons[k] = new Neuron(k, 1, 4);
                    // set positions
                    neurons[k].X = (float)(i * Step);
                    neurons[k].Y = (float)(j * Step);
                    neurons[k].Z = 0f;
                    // next neuron
                    k++;
                }
            }
        }
        /// <summary>
        /// Sets Input Signal to inputs neurons
        /// </summary>
        /// <param name="input">1-black, 0-white </param>
        public void SetInputSignal(ref short[] input)
        {
            for (int i = 0; i < inputsCount; i++)
            {
                //TODO change input sheme
                // ! Input index equal with the neuron number
                neurons[i].SetInput(input[i]);
            }
        }
        public void SetInputSignal(ref short[,] input)
        {
            int k = 0;
            for (int i = 0; i < inputSize; i++)
                for (int j = 0; j < inputSize; j++)
                {
                    //TODO change input sheme
                    // ! Input index equal with the neuron number

                    //Debug.Write(k, "neuron");
                    neurons[k++].SetInput(input[i, j]);
                    //Debug.WriteLine(input[i, j], " set input");
                }
        }
        public void SetInputSignal(int[] inpuNeurons, int[] Signals)
        {
            //TODO eliminate this
            Debug.Assert(inpuNeurons.Length == Signals.Length, "Set Inp Signal different arrays length");
            int i = 0;
            foreach (int neuron in inpuNeurons)
            {
                //TODO Short to float??
                neurons[neuron].SetInput((short)Signals[i++]);
            }
        }
        /// <summary>
        /// set output signal for each outputs(end) neurons
        /// </summary>
        /// <param name="output">value of output 1 to 256</param>
        //TODO output value must be single
        public void SetOutputSignal(ref short[] output)
        {
            for (int i = 0; i < outputsCount; i++)
            {
                //TODO change input sheme
                // ! Input index equal with the neuron number
                //Debug.WriteLine(inputsCount + basicNeuronsCount + i, "set output");
                neurons[inputsCount + basicNeuronsCount + i].SetOutput(output[i]);
            }
        }
        /// <summary>
        /// compue neurons state for one neurons activity wave
        /// </summary>
        /// <param name="input">input matrix 0-black, 1-white</param>
        /// <param name="output">output vektor that accord to input matrix</param>
        public void Tick(ref short[,] input, ref short[] output)
        {
            LifeTime++;
            // TODO reset activation state or compute to another state: low
            Reset();
            // set input signal
            SetInputSignal(ref input);
            // set output (activated output neurons)
            //TODO learn change
            //          SetOutputSignal(ref output);
            // compute neurons state
            // activation welle 
            Compute();
            // learn 
            // make connections -> new neurons -> new axons connect to dendrites on activ neurons
            Connect();
        }
        public void Tick(ref short[,] input, ref int[] inputNeurons, ref int[] Signals)
        {
            Debug.Assert(inputNeurons.Length == Signals.Length, "Set Inp Signal different arrays length");
            LifeTime++;
            // TODO reset activation state or compute to another state: low

            Reset();
            // set input signal
            SetInputSignal(ref input);
            //set input signal another inputs
            SetInputSignal(inputNeurons, Signals);
            // set output (activated output neurons)
            // compute neurons state
            // activation welle 
            Compute();
            // learn 
            // make connections -> new neurons -> new axons connect to dendrites on activ neurons
            Connect();


        }

     

        /// <summary>
        /// lernen nach Positives Ereignis
        /// </summary>
        public void learn(float learnWeght)
        {
            // for first frames
            if (ActivityQueue[0] == null) return;

            for (int i = 0; i < QueueCount; i++)
            {
                // for all frames
                int length = ActivityQueue[i].Length;
                // for all activ neurons
                for (int j = 0; j < length; j++)
                {
                    int activNeuron = ActivityQueue[i][j];
                    int[] ingoing;
                    int[] outgoing;
                    neurons[activNeuron].getConnections(out ingoing, out outgoing);
                    // grow input weights for connected and activ neurons
                    foreach (int k in outgoing)
                    {
                        for (int m = 0; m < length; m++)
                            if (ActivityQueue[i][m] == k)
                                neurons[k].strengthInputs(activNeuron, learnWeght);
                    }
                }
            }
        }
        /// <summary>
        /// compute network state for input to output relation
        /// </summary>
        private void Compute()
        {
            // activation wave
            // computate so many times that all of neurons will be computated
            // computation wave can be not sequent
            ComputatedNeurons.Clear();
            ActivatedNeurons.Clear();
            for (int iWave = 0; ComputatedNeurons.Count < NeuronsCount; iWave++)
            {
                ActivationWave(iWave);
                Debug.WriteLine(iWave, " wave");
                Debug.WriteLine(ComputatedNeurons.Count, " computated neurons");
                Debug.WriteLine(NeuronsCount, " from");

            }
        }
        /// <summary>
        /// compute activation wave. It will be all neurons computated thay are have 
        /// </summary>
        /// <param name="iWave">wave count for this tick</param>
        private void ActivationWave(int iWave)
        {
            foreach (Neuron neuron in neurons)
            {
                if (neuron.Computate())
                {
                    ComputatedNeurons.Add(neuron.ID);
                    if (neuron.Activated)
                    {
                        ActivatedNeurons.Add(neuron.ID);
                    }

                    //Debug.WriteLine(neuron.ID, " computated neuron");
                    //signal weitergeben
                    foreach (Axon a in neuron.outputs) neurons[a.ConnectedNeuron].ActivateInput(a.ConnectedDendrit, a.Signal);
                }
                //event for wave
                //      NeuroWaveComputed(this, "wave " + iWave.ToString() + " computed");
            }
        }
        /// <summary>
        /// make new neurons and connections (see Embryos)
        /// </summary>
        private void Connect()
        {
            //last waves for active neurons memorise
            ActivityMemorize();
            //check embryos to be born
            //           checkForNewEmbryos(ActivatedNeurons);
            //let's embryos born
            //           letsEmbryosBorn();


        }
        /// <summary>
        /// check the embryos list to make new neurons from embryos
        /// </summary>
        /// <param name="activatedNeurons">list of activated neurons</param>
        private void checkForNewEmbryos(List<int> activatedNeurons)
        {
            List<int> neuronsToSearch = new List<int>();
            //filter nach ebenen 
            foreach (int n in activatedNeurons)
                if (neurons[n].Z < 0.7f) neuronsToSearch.Add(n);

            if (EmbryoNeurons.Count == 0)
                //while there are any activated neurons (just for first call) in a tick or wave
                while (neuronsToSearch.Count > 0)
                    //TODO create embryo just from nearest neurons
                    EmbryoNeurons.Add(new EmbryoNeuron(ref neuronsToSearch));
            else
            {
                foreach (EmbryoNeuron i in EmbryoNeurons)
                {
                    //if yes embryo must grow
                    i.checkGrow();

                }

                foreach (EmbryoNeuron i in EmbryoNeurons)
                {
                    //TODO must redefine function IsIn dependet from coordinates
                    //check if activated neurons are a part of existing embryos ,
                    if (i.IsIn(neuronsToSearch))
                    {
                        //if yes embryo must grow
                        i.Grow();
                        //remove embryo from list of actives (spin) neurons for this tick
                        //TODO active neurons give signal just for one embryo?
                        i.RemoveFrom(ref neuronsToSearch);
                    }
                }

                //create new embryos
                //TODO condition  that in Embryo must have more than 1 neurons is natural
                //RES speed opt
                while (neuronsToSearch.Count > 1)
                {
                    EmbryoNeurons.Add(new EmbryoNeuron(ref neuronsToSearch));
                }
            }
            return;
        }
        private void letsEmbryosBorn()
        {
            List<EmbryoNeuron> EmbyToRemove = new List<EmbryoNeuron>();

            foreach (EmbryoNeuron i in EmbryoNeurons)
            {
                if (i.LifeTime > maxEmbryoLifeTime) EmbyToRemove.Add(i);
                if (i.ReadyToBorn & !i.wasBorn)
                {
                    //TODO 1.5f und Embryo was verworfen wenn keine freie outputs
                    this.AddNeuron(i.Connections, new Neuron(this.NeuronsCount, i.Connections.Length, 4), 1.5f);
                    i.wasBorn = true;
                    Debug.WriteLine(i.ID, "embryo was born");
                    //remove embryo from list, it was born
                    EmbyToRemove.Add(i);
                }
            }

            foreach (EmbryoNeuron e in EmbyToRemove)
                EmbryoNeurons.Remove(e);

        }
        private void trimmNeurons(int nue)
        {
            foreach (Neuron n in neurons)
            {
                foreach (Axon a in n.outputs)
                {
                    if (neurons[a.ConnectedNeuron].inputs[a.ConnectedDendrit].Weight == 0.0f)
                        ;
                }
            }
        }
        unsafe private void cuda_check_embryos()
        {
            //Zwischenspeicherung pointers für dev_pointers
            //TODO Andere loesung
            cuPointers dev_ptr;

            int[] activated_neurons = new int[ActivatedNeurons.Count];
            cuEmbryo[] embryos = new cuEmbryo[EmbryoNeurons.Count];

            for (int i = 0; i < ActivatedNeurons.Count; i++)
                activated_neurons[i] = ActivatedNeurons[i];

            int m = 0;
            for (int i = 0; i < EmbryoNeurons.Count; i++)
            {
                embryos[i].was_activ = EmbryoNeurons[i].WasActiv;
                m = embryos[i].inputs_count = EmbryoNeurons[i].Connections.Length;
                for (int j = 0; j < m; j++)
                    fixed (int* conn = embryos[i].connections)
                    {
                        conn[j] = EmbryoNeurons[i].Connections[j];
                    }
            }

            fixed (int* ptr_activated_neurons = activated_neurons)
            {
                fixed (cuEmbryo* ptr_embryos = embryos)
                {

                    dev_ptr = check_embryos(
                        ptr_activated_neurons,
                        ActivatedNeurons.Count,
                        ptr_embryos,
                        EmbryoNeurons.Count);

                    //                   Compute();

                    end_check_embryos(
                        dev_ptr.n,
                        ptr_embryos,
                        dev_ptr.e,
                        EmbryoNeurons.Count);

                    for (int i = 0; i < EmbryoNeurons.Count; i++)
                    {
                        EmbryoNeurons[i].WasActiv = embryos[i].was_activ;
                    }
                }
            }

        }
        /// <summary>
        /// save all activity of neurons during lifetime
        /// </summary>
        private void ActivityMemorize()
        {
            //Neurons Activity aufnehmen FIFO
            for (int i = 0; i < QueueCount - 1; i++)
                ActivityQueue[i] = ActivityQueue[i + 1];

            ActivityQueue[QueueCount - 1] = new int[ActivatedNeurons.Count];
            ActivatedNeurons.CopyTo(ActivityQueue[QueueCount - 1]);

        }
        /// <summary>
        /// reset network for this tick
        /// </summary>
        private void Reset()
        {
            //clear list of computated neurons 
            ComputatedNeurons.Clear();

            ActivatedNeurons.Clear();
            //TODO ActivatedNeuronsFrequency -> clear connected neurons
            //TODO That is false! Neurons must go out naturally. Make a subprog EndComputate();
            foreach (Neuron neuron in neurons) neuron.Reset();
        }
        public void groupNeuronsAt(int[] neuronsToGroup, float X, float Y, float Z, float xRange, float yRange)
        {
            for (int i = 0; i < neuronsToGroup.Length; i++)
            {
                neurons[neuronsToGroup[i]].X = ((float)random.NextDouble() - 0.5f) * xRange + X;
                neurons[neuronsToGroup[i]].Y = ((float)random.NextDouble() - 0.5f) * yRange + Y;
                neurons[neuronsToGroup[i]].Z = Z;
            }
        }
        /// <summary>
        /// add new neuron to network
        /// </summary>
        /// <param name="inputConnections">index of neurons connected for input </param>
        /// <param name="neuron">new neuron</param>
        /// <param name="zOffset">distanse of z-achse for the new neuron</param>
        /// <returns>neurons ID number</returns>
        public int AddNeuron(int[] inputConnections, Neuron neuron, float zOffset)
        {
            NeuronsCount++;
            if (neurons.Length < NeuronsCount)
            {
                //RES too slow
                Neuron[] newNeuronsArray = new Neuron[NeuronsCount];
                neurons.CopyTo(newNeuronsArray, 0);
                neurons = newNeuronsArray;
            }

            neurons[NeuronsCount - 1] = neuron;
            float Xc = 0f, Yc = 0f, Zc = 0f;

            foreach (int i in inputConnections)
            {
                if (!neuron.ConnectToOutputOf(ref neurons[i]))
              
                    {
                        Debug.WriteLine(neuron.ID, "can't connect neuron");
                        //suchen freie
                    }
                Xc += neurons[i].X;
                Yc += neurons[i].Y;
                Zc += neurons[i].Z;
            }

            if (neuron.X == 0f & neuron.Y == 0f)
            {
                neuron.X = Xc / (float)inputConnections.Length;
                neuron.Y = Yc / (float)inputConnections.Length;
                neuron.Z = Zc / (float)inputConnections.Length + zOffset;
            }

            return neuron.ID;
        }
        public int AddNeuron(Neuron neuron)
        {
            NeuronsCount++;
            if (neurons.Length < NeuronsCount)
            {
                //RES too slow
                Neuron[] newNeuronsArray = new Neuron[NeuronsCount];
                neurons.CopyTo(newNeuronsArray, 0);
                neurons = newNeuronsArray;
            }

            neurons[NeuronsCount - 1] = neuron;
 
           return neuron.ID;
        }
        /// <summary>
        /// search nearest neurons in range
        /// </summary>
        /// <param name="Range">range</param>
        /// <returns>array of indexes</returns>
        public int[] findNeuronsInRange(float X, float Y, float Z, float Range)
        {
            List<int> nearestNeuronsList = new List<int>();
            foreach (Neuron n in neurons)
            {
                if (n.inRange(X, Y, Z, Range)) nearestNeuronsList.Add(n.ID);
            }

            int[] nearestNeurons = new int[nearestNeuronsList.Count];

            nearestNeuronsList.CopyTo(nearestNeurons);

            return nearestNeurons;

        }
        /// <summary>
        /// Save network to specified file.
        /// </summary>
        /// 
        /// <param name="fileName">File name to save network into.</param>
        /// 
        /// <remarks><para>The neural network is saved using .NET serialization (binary formatter is used).</para></remarks>
        /// 
        public void Save(string fileName)
        {
            FileStream stream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None);
            Save(stream);
            stream.Close();
        }
        /// <summary>
        /// Save network to specified file.
        /// </summary>
        /// <param name="stream">Stream to save network into.</param>
        /// <remarks><para>The neural network is saved using .NET serialization (binary formatter is used).</para></remarks>
        public void Save(Stream stream)
        {
            IFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, this);
        }
        /// <summary>
        /// Load network from specified file.
        /// </summary>
        /// <param name="fileName">File name to load network from.</param>
        /// <returns>Returns instance of <see cref="Network"/> class with all properties initialized from file.</returns>
        /// <remarks><para>Neural network is loaded from file using .NET serialization (binary formater is used).</para></remarks>
        public INetwork Load(string fileName)
        {
            FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);

            Network network = new Network();
            //          INetwork network;
            network = Load(stream);
            stream.Close();

            return network;
        }
        /// <summary>
        /// Load network from specified file.
        /// </summary>
        /// <param name="stream">Stream to load network from.</param>
        /// <returns>Returns instance of <see cref="Network"/> class with all properties initialized from file.</returns>
        /// <remarks><para>Neural network is loaded from file using .NET serialization (binary formater is used).</para></remarks>
        public static Network Load(Stream stream)
        {
            IFormatter formatter = new BinaryFormatter();
            Network network = (Network)formatter.Deserialize(stream);
            return network;
        }
        public int getConnectionsCount()
        {
            int n = 0;
            foreach (Neuron i in neurons)
                foreach (Dendrit d in i.inputs)
                    n++;

            return n;
        }

    }

}
